This page describes the format of messages exchanged between the various LoRa sensors and their Gateway. In today's world we often use JSON messages to code complex structure into readible format and exchange these messages over the internet. However, LoRa has different requirements. As LoRa nodes are not supposed to talk more than 1% of the time, sending messages as short as possible makes sense.
So where we normally would send the temperature like this:
{ "temperature": "20.41" }
it makes sense to shorten that message as much as possble so we can send more messages in the available 1% of time. So a better way would be:
{"t":"20.41"}
And assuming we know that it is the temperature that we're receiving in 2 integers and a fraction of 2 digit, all would fit in 2 bytes:
"0x14 0x29"
However, such shortcuts only work it both sender and receiver know what's coming and they will not send other messages for other sensor values. Because if they do, there is a need to make these messages a little more descriptive, yet as short as possible.
Although heavily discouraged by the community it is safe to assume that some messages will be JSON still, but preferrable we should code our messages such that they contain as less space as possible.
Header | Sensor values [1:n] | |||
---|---|---|---|---|
Start bit==1 | Length of message including header | Parity bit | Opcode byte | Value Byte(s) [1:n] |
The parity bit is set after all sensors values are added to the message. We use even parity (in other words: the sum of all 1-bits in the message must be even). As LoRa itself uses error checking algorithms we decided not to add any more to the message by implementing a more sophysticated error checking and correcting scheme.
The Sensor Values are encoded as follows:
OpCode Byte | Value Bytes [1:n} | |
---|---|---|
6-bit ID | 2-bit Length | 1-4 byte value * |
(*) Some OpCodes such as GPS and MultiButton use more than 4 bytes to encode their value. In this case, the length field is not used (don't care) and the number of value bytes is defined completely by the opCode.
Based on this format he following message types (Id's) are defined:
Sensor Id 6-bit Len 2-bit Encoding Temperature (-100-150 degrees C) 0b0000 01 0b01 (+1) byte[0] = INT(t +100); byte[1]=2 digit fraction (00-99)
result = (byte[0] - 100) + (byte[1] / 100)
Humidity (0-100%) 0b0000 10 0b00 (+1) byte[0] = INT(Humidity * 2)
result = byte[0] / 2 (as %)
Airpressure (800-1104 hPa) 0b0000 11 0b00 (+1) byte[0] = (Airpressure in hPa - 850)
result = byte[0] + 850
GPS Short Info 0b0001 00 d.c. byte[0-2] = LAT, byte[3-5]=LNG GPS Long Info 0b0001 01 d.c. byte[0-3] = LAT,
byte[4-7] = LNG,
byte[8-11]=ALT,
byte[12-15] =UTime,
byte[16] = nr of satellitesPIR 0b0001 10 0b00 (+1) byte[0]=0x00 off, 0x01 armed, 0x02 on, 0x03 sent
result = byte[0]
AirQuality 0b0001 11 0b01 (+1) byte[0-1] = AQ (most signicicant byte first)
result = (byte[0] *256) + byte[1]
Real-Time Clock (RTC) 0b0010 00 0b11 (+1) byte[0-[3] unsigned long value of secs since 1-1-1970 Compass Sensor (X,Y,Z) 0b0010 01 t.b.d. Multi-Button 0b0010 10 d.c. byte[0-3] = Address
byte[4-5] = Unit
total 6 bytes: 4 bytes address and 2 byte unit code of button code activatedMoisture 0b0010 11 0b00 byte[0] = M / 4;
One Byte moisture (value 0-1023) / 4. 255 is dry, 0 is super wet.Luminescense 0b0011 00 0b01 Two bytes with luminescense code. 0 is dark, 3999 is bright sunlight.
result = (byte[0]*256)+byte[1]) / 10 Lux
Distance 0b001101 0b01 byte[0-1]=Distance 0-65,535
result = (byte[0] * 256) + byte[1] cm
Battery condition (0-12 V) 0b1000 00 0b00 (+1) byte[0] = INT (V * 20)
result = byte[0] / 20 Volt
AD converter A0 (256 steps) 0b1000 01 0b00 AD converter A1 (256 steps) 0b1000 02 0b00 User Codes General Integer 0b1000 01 0b00,
0b01,
0b11uint8_t (1 byte),
uint16_t (2 bytes),
uint32_t (4 bytes)General Character String 0b1000 10 0b00 byte[0] defines length of char string. Note: Total message size <128 chars Downlink Codes Status 0b1100 00 0b00 Ask for status of the node SF 0b1100 01 0b00 One byte is spreading factor 0x07 - 0x0C Timing for messages 0b1100 10 0b01 2 bytes timing in seconds Other ID's are free to use
Notes:
Where it makes sense, the two last bits of the opcode byte are used to express the number of bytes that represent the sensor value and that are following the opcode byte. For example: For humidity we use one byte to encode the sensor value. Therefore the last two bits of the opcode byte are 0x00 (+1 = 1 byte).
For some sensors such as GPS we need more information than just the 3 bytes that can be coded in 2 bits. As we use fixed amount of fields and bytes to encode the GPS coordinates, this does not matter. So for these devices the amount of bytes needed is found in the table above.
This section contains a few examples that show how the lCode library encodes (and decodes) the various message formats.
Imagine a simple sensor transmitting a battery value of 3.2Volt (float). The message payload sent by LoRa is coded in three bytes as follows:
0x87 0x80 0x40 which is in binary format: 1000 0111 1000 0000 0100 0000
The start byte 0x87 consists of
The second opcode byte 0x80 has binary value 1000 0000, consisting of a command id of 100000 (O_BAT) and two length bits that are zero (0x00). As the length bits are zero this means that one byte value will follow for this sensor value.
The third byte with value 0x40 has decimal value 64. As the battery value was multiplied by 20 it means that the battery value measured was 3.2V.
This second example handles downlink messages. Just like uplink messages that are sent by the node containing sensor data, downlink messages contain commands and information from the LoRa server (/router).
1. Ask for node status: 0x84 0xC0 computed as follows: ( 0x80 | 0x02<< 1 ) ; ( 0x03<<2 | 0x00 )
2. Set the Spreading Factor to 7: 0x86 0xC4 0x07
3. Set the timing between messages to 32 seconds: 0x88 0xC8 0x00 0x20
The following opcodes are defined both for the node (IDE library) and for the backend:
#define O_TEMP 0x01 // Temperature is a one-byte code
#define O_HUMI 0x02 // Humidity is a one-byte code
#define O_AIRP 0x03 // Air pressure is a one-byte code
#define O_GPS 0x04 // Short version: ONLY 3 bytes LAT and 3 bytes LONG
#define O_GPSL 0x05 // Long GPS
#define O_PIR 0x06 // Movement, 1 bit (=1 byte)
#define O_AQ 0x07 // Airquality
#define O_RTC 0x08 // Real Time Clock
#define O_COMPASS 0x09 // Compass
#define O_MB 0x0A // Multi Sensors 433
#define O_MOIST 0x0B // Moisture is one-byte
#define O_LUMI 0x0C // Luminescense u16
#define O_DIST 0x0D // Distance is 2-byte
#define O_GAS 0x0E // GAS
/* 0x10 to 0x1F are free */
#define O_BATT 0x20 // Internal Battery
#define O_ADC0 0x21 // AD converter on pin 0
#define O_ADC1 0x22
// Reserved for LoRa messages (especially downstream)
#define O_STAT 0x30 // Ask for status message from node
#define O_SF 0x31 // Spreading factor change OFF=0, values 7-12
#define O_TIM 0x32 // Timing of the wait cyclus (20 to 7200 seconds)
#define O_1CH 0x33 // Single channel ON=1, OFF==0
#define O_LOC 0x34 // Ask for the location. Responds with GPS location (if available)
For encoding sensor values in the LoRa payload, a simple set of library functions is developed that make adding values to the payload very easy. The libary LoRaCode contains 2 files: LoRaCode.h and LoRaCode.cpp.
The following encoding functions are present in the Arduino LoRaCode library: (LoRaCode.h):
int eTemperature(float val, byte *msg);
int eHumidity(float val, byte *msg);
int eAirpressure(float val, byte *msg);
int eGps(double lat, double lng, byte *msg);
int eGpsL(double lat, double lng, long alt, int sat, byte *msg);
int ePir(int val, byte *msg);
int eAirquality(int val, byte *msg);
int eRtc
int eMbuttons(byte val, unsigned long address, unsigned short channel, byte *msg);
int eMoist(int val, byte *msg);
int eLuminescense(int val, byte *msg);
int eBattery(float val, byte *msg);
Downlink messages are (de-)coded as follows:
int LoRaCode::dMsg (byte *msg, byte *val, byte *mode) {}
Also, two supporting functions are defined. eMsg will analyse the message buffer after all sensors are added. It will compute the total length of the buffer and add parity bit when necessary. The lPrint function will print the buffer in hexadecimal format.
bool eMsg(byte *msg, int len);
void lPrint(byte *msg, int len);
Decoding can be done for every language. We chose to decode in Node.js (JavaScript) which makes adding fields to an MQTT message very easy. Once all fields are decoded the resulting object can be sent with MQTT without problems (as a JSON object). Based on the node.js library it is easy to port this library to other languages.
The dCode function in the lCode module will take care of the decoding of the lCode messages. The result returned by the dCode function is an object with fields that represent the sensor values that were present in the message and successfully decoded. The following fields can be added to the object (only the fields that are decoded will be present):
var LoRaMsg = require(par.homeDir+'/modules/lCode');
var sensorData = LoRaMsg.dCode(DevAddr,dataRaw);
// sensorData has several field after LoRaMsg.dCode() has finished
sensorData = {
temperature: <value>,
humidity: <value>,
airpressure: <value>,
gps: {
lat: <value>,
lng: <value>
},
gps = {
lat: <value>,
lng: <value>,
alt: <value>,
sat: <value>
}
pir: <value>,
airquality: <value>,
button: <value>,
b_addr: <value>, // Only present for multi-button concentrator
b_unit: <value> , // idem
moist: <value>,
luminescense: <value>,
battery: <value>
};
Although using this lCode message format does not result in the shortest messages one can send over LoRa, the overhead of one length byte per message and one ID byte per sensor value is low compared to the full message sent over the air (containing all sorts of header and address data as well).
So therefore we will use this message format in most our Arduion/ESP8266 based sensor nodes as well as in our own node.js backend.